Tutoriel REPL

Tutoriel REPL Haskeline (Amélioration)

Le code source de cet exemple peut être téléchargé ici.

Introduction

Nous avons réalisé dans le tutoriel "Tutoriel REPL Haskeline" une boucle REPL avec une ligne de commandes assez complète avec Haskeline.

Néanmoins, deux petits problèmes subsistent.

  • L'enregistrement de l'historique des commande ne se fait que lorsque le programme se termine correctement et cela peut être problématique si on veut pouvoir visualiser les commandes introduites après un crash du programme.

  • Le fichier d'historique contient les commandes dans l'ordre allant de la plus récente à la plus ancienne, ce qui est assez contre intuitif.

Nous allons donc voir comment utiliser certaines fonctions "bas niveau" de la bibliothèque Haskeline afin de corriger ces deux défauts.

Seulement … voila ! Ces fonctions sont faites pour fonctionner à l'intérieur de la monade InputT, il va donc falloir d'abord réécrire notre programme pour le faire tourner au-dessus de la monade InputT.

Conversion vers la monade InputT

La conversion de notre programme est très facile, il suffit de lancer la monade InputT avec la fonction runInputT avec comme arguments nos préférences et la fonction principale.

main = do
    runInputT mySettings $ do
        outputStrLn $ unlines help
        replLoop 1

Il faut ensuite utiliser la fonction liftIO du module Control.Monad.Trans pour pouvoir lancer des fonctions d'entrée sortie de la monade IO. Les fonctions putStrLn peuvent être remplacées par la fonction outputStrLn de la bibliothèque Haskeline.

replEval com@(':' : 'l' : 's' : _ ) = do
    dir     <- liftIO getCurrentDirectory
    content <- liftIO $ getDirectoryContents dir
    let filteredContent = sort $ filter (\f -> notElem f [".", ".."]) content
    return filteredContent


replEval com@(':' : 'h' : 'e' : 'u' : 'r' : 'e' : _ ) = do
    tim  <- liftIO getCurrentTime
    zone <- liftIO getCurrentTimeZone
    let (TimeOfDay h m s) = localTimeOfDay $ utcToLocalTime zone tim
    return ["Il est " ++ show h ++ " heures " ++ show m ++ " minutes " ++ show s ++ " secondes"]

Notre programme est maintenant prêt pour les modifications que l'on veut apporter.

Enregistrement des commandes

Désactivation de l'historique par défaut

Pour commencer nous allons désactiver la création automatique de l'historique dans les préférences de la ligne de commande.

mySettings = Settings
    { complete       = completeWord Nothing "" searchFunc
    , historyFile    = Nothing
    , autoAddHistory = True
    }

Récupération de l'historique

Pour récupérer le fichier d'historique, on commence par vérifier son existence avec la fonction doesFileExist du module System.Directory.

Si le fichier n'existe pas, on définit un historique vide emptyHistory de la monade avec la fonction putHistory.

Si le fichier existe, on lit le fichier avec readFile et on le décompose en lignes avec lines. On effectue ensuite des ajouts successifs à un historique vide avec addHistory et foldr avec la liste inversée des lignes du fichier d'historique. On définit ensuite l'historique de la monade avec putHistory.

main = do
    runInputT mySettings $ do
        ex <- liftIO $ doesFileExist "history.hist"
        if ex
            then do
                lns <- liftIO $ lines <$> readFile "history.hist"
                let hist = foldr addHistory emptyHistory (reverse lns)
                putHistory hist
            else do
                putHistory emptyHistory
        outputStrLn $ unlines help
        replLoop 1

Enregistrement des commandes

Pour enregistrer les commandes dans le fichier d'historique, on fait un ajout dans celui-ci avec appendFile après chaque invocation de getInputLine. On n'oublie pas d'ajouter un retour à la ligne à chaque fois si on ne veut pas se retrouver avec un fichier d'une seule ligne.

replRead i = do
    com <- getInputLine ("ma commande " ++ show i ++ " >")
    case com of
        Just c  -> liftIO $ appendFile "history.hist" (c ++ "\n")
        Nothing -> return ()
    return com

Problème d'accès au fichier d'historique

Lorsque l'on lance le programme et que l'on commence à taper des commandes, on constate que le programme plante avec un message d'erreur du type:

REPL3_fr: history.hist: openFile: resource busy (file is locked)

Ce problème est lié au fonctionnement de Haskell et plus particulièrement à l'évaluation paresseuse (lazy).

En effet, Haskell ne lit pas le fichier historique au début du programme mais seulement lorsqu'il en a besoin. C'est à dire, dans le cas présent, lorsque l'on veut y accéder pour écrire une ligne. Ce qui cause une erreur fatale.

Pour éviter cela, plusieurs solutions peuvent être envisagées:

  • Utiliser une version stricte de readFile, mais elle n'est pas toujours disponible.

  • Forcer la lecture du fichier au début du programme en effectuant une opération "bidon" sur le contenu du fichier.

    On peut par exemple calculer le nombre de lignes contenu dans le fichier avec evaluate du module Control.Exception et ne pas utiliser le résultat.

    _ <- liftIO $ evaluate (length lns)
    

    Ou alors profiter de la lecture du fichier pour afficher un message d'information sur le nombre de lignes récupérées du fichier.

    outputStrLn $ "Chargement de " ++ show (length lns) ++ " lignes d'historique du fichier : "
    

    Cette solution étant la plus facile à mettre en oeuvre.

main = do
    runInputT mySettings $ do
        ex <- liftIO $ doesFileExist "history.hist"
        if ex
            then do
                lns <- liftIO $ lines <$> readFile "history.hist"
                let hist = foldr addHistory emptyHistory (reverse lns)
                -- ~ _ <- liftIO $ evaluate (length lns)
                outputStrLn $ "Chargement de " ++ show (length lns) ++ " lignes d'historique du fichier : "++ "history.hist"
                putHistory hist
            else do
                putHistory emptyHistory
        outputStrLn $ unlines help
        replLoop 1

Et maintenant tout fonctionne parfaitement.

Conclusion

Voilà, c'est fini ! J'espère que ces petits tutoriels sur Haskeline et les boucles REPL vous seront utiles et vous permettront de développer de beaux programme en ligne de commandes.